<?php
namespace system\libraries;

use system\libraries\acl\Role;
use system\libraries\Session;
use system\core\Config;
use system\core\Request;

class Acl 
{
    /**
     * Stores the Role objects
     * 
     * @var system\libraries\acl\Role
     */
    private $_roles = array();
    
    /**
     * Default Acl role
     * 
     * @var string
     */
    private $_defaultRole = 'guest';
    
    /**
     * Redirect url
     * 
     * @var string
     */
    private $_redirectUrl;
    
    /**
     * Stores an instance of Acl
     *  
     * @var system\libraries\Acl
     */
    private static $_instance;
    
    /**
     * Constructor
     * 
     * @return system\libraries\Acl
     */
    public function __construct()
    {
        self::$_instance = $this;
        
        return self::$_instance;
    }
    
    /**
     * Get the last Acl instance
     * 
     * @return system\libraries\Acl
     */
    public static function getInstance()
    {
        return self::$_instance;
    }
    
    /**
     * Set the default role
     * 
     * This role will be used when there was no session found with a role id.
     * 
     * @param string $role
     */
    public function setDefaultRole( $role )
    {
        $this->_defaultRole = $role;
    }
    
    /**
     * Add a Role
     * 
     * @param string $role
     * @param string $inheritor
     * @return system\libraries\acl\Role
     */
    public function addRole( $role, $inherits = NULL )
    {
        if ( in_array($role, $this->_roles) )
        {
            throw new \Exception('Could not add role "' . $role . '", role does already exist.');
        }
        
        if ( $inherits !== NULL )
        {
            if ( ! in_array($inherits, $this->_roles) )
            {
                throw new \Exception('Could not inherit from role "' . $inherits . '", role does not exist.');
            }
        }
        
        return $this->_roles[] = new Role($role, $inherits);
    }
    
    /**
     * Allow a Resource to a Role
     * 
     * @access public
     * @param string $role
     * @param string $controller
     * @param mixed $actions 
     */
    public function allow( $role, $controller = NULL, $actions = NULL )
    {
        $roleName = (string)$role; // We use the magic __toString()
        
        // Does this role exists?
        if ( ! in_array($roleName, $this->_roles) )
        {
            throw new \Exception('The role "' . $role . '" does not exist.');
        }
        
        // Get the corresponding array key of this role
        $key = array_search($roleName, $this->_roles);
        
        foreach ( $this->_roles as $_key => $_role )
        {
            // If this role is lower then the allowed role, just continue to
            // the next one..
            if ( $_key < $key ) { continue; }
            
            $_roleName = (string)$_role;
            
            // Does this role inherits an existing role?
            if ( in_array($_role->getInherits(), $this->_roles) 
                    && $role != $_roleName )
            {
                // well, lets add the resources to it
                $_role->addResource( $controller, $actions );
            }
            else
            {
                // Break this loop if the allowed role is not the same as
                // the role in the loop!
                if ( $_roleName != $role ) { break; }
                $_role->addResource( $controller, $actions );
            }
        }
    }
    
    /**
     * Deny a resource
     * 
     * @access public
     * @param string $role
     * @param mixed $controller
     * @param mixed $acions 
     */
    public function deny( $role, $controller = NULL, $actions = array() )
    {
        $roleName = (string)$role; // We use the magic __toString()
        
        // Does this role exists?
        if ( ! in_array($roleName, $this->_roles) )
        {
            throw new \Exception('The role "' . $role . '" does not exist.');
        }
        
        $this->_getRole($role)->denyResource($controller, $actions);
    }
    
    /**
     * Check if the role is allowed to access this resource
     * 
     * @access public
     * @param string $role
     * @param string $controller
     * @param string $action
     * @return bool
     */
    public function isAllowed( $role, $controller, $action, $parameter = NULL )
    {
        $session = Session::getInstance();
        
        // If the role does not exist, destroy any existing session
        if ( ! in_array($role, $this->_roles) )
        {
            $session->destroy();
            throw new \Exception('Acl role "' . $role . '" does not exist. All sessions will be destroyed.');
        }
        
        $defaultController = strtolower(Config::get('config', 'default_controller'));
        $defaultAction     = strtolower(Config::get('config', 'default_action'));
        
        if ( $defaultController == strtolower($controller) && $defaultAction == strtolower($action) )
        {
            return TRUE;
        }
        
        return $this->_getRole($role)->hasResource($controller, $action, $parameter);
    }
    
    /**
     * Set the redirect url
     * 
     * This is the url were the visitor will be redirected to when he is not
     * allowed to access that resource.
     * 
     * @access public
     * @param string $url
     */
    public function setRedirectUrl( $url )
    {
        $this->_redirectUrl = $url;
    }
    
    /**
     * Get the default role
     * 
     * @return string
     */
    public function getDefaultRole()
    {
        return $this->_defaultRole;
    }
    
    /**
     * Get the redirect url
     * 
     * @access public
     * @return string
     */
    public function getRedirectUrl()
    {
        return $this->_redirectUrl;
    }
    
    /**
     * Get the role object from the roles array property
     * 
     * @access public
     * @param string $role
     * @return array
     */
    private function _getRole( $role )
    {
        return $this->_roles[ array_search($role, $this->_roles) ];
    }
    
    /**
     * Check Acl
     * 
     * This method checks the whole ACL and redirects when access is denied.
     * 
     * @access public
     */
    public function checkAcl( $function = NULL )
    {
        $session = Session::getInstance();   
        $request = Request::getInstance();

        $sessionName = Config::get('acl', 'session_key');

        $role = ( $session->data($sessionName) ) ? $session->data($sessionName) : $this->getDefaultRole();
        
        $parameters = $request->getParameters();
        $parameter = ( isset($parameters[0]) ) ? $parameters[0] : NULL;
        
        if ( ! $this->isAllowed($role, $request->getController(), $request->getAction(), $parameter) )
        {
            if ( $function )
            {
                call_user_func($function, $role, $request, $session);
                die();
            }

            header('Location: ' . $this->getRedirectUrl(), TRUE);
            die();
        }
     }
}

/* End of file Acl.php */
